<?php
/**
	功能: 基于PDO的数据库操作类, dbox的升级版
	版本：3.0
	作者:yf
	日期:2018-07-04

	what's new:
		数据库配置参数外置，每个实例带有各自的数据库连接参数，适用项目中同时操作多个数据库
		fetch_page方法提高了查询效率

	修改：2018/4/7，
	- 增加了多个方法的debug参数，用于显示最终的sql
	- 增加查询语句带 ? 参数的支持

 */


class dbox3 {

	protected $dbh; //PDO instance!
	protected $sth; //PDO statement


	/*	获取数据操作对象，连接失败则返回false!
		参数为关联数组，结构如下：
		[	'dsn'=>'mysql:host=127.0.0.1;port=3306;dbname=mydatabase;charset=utf8', //数据库连接字串
			'user'=>'root',			//用户名
			'pass'=>'123456',		//密码
			'options'=>[			//初始化参数，具体可参考PDO::setAttribute()文档，常用参数如下：
				PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',	//mysql 5.3.6之前的版本，dsn中的charset参数无效，需要用此参数设定
				PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,			//出错不抛异常，仅设置错误代码
				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,		//出错抛异常
				PDO::ATTR_STRINGIFY_FETCHES => false,				//提取数据时，数字是否转换为字符串；//有时无效，原因待查
				PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,	//设置默认获取数据结果的类型为关联数组
			],

		]
	 */
 	static public function getInstance($p) {
		try {
			return new self ($p);
		}catch(PDOException $e){
			return false;
		}
	}

	protected function __construct($p){
		$this->dbh=new PDO($p['dsn'], $p['user'], $p['pass'],$p['options']);
	}

	//获取PDO对象，方便用户作扩展操作
	public function getPDO(){
		return $this->dbh;
	}

	//获取 PDOStatement 对象，方便用户作扩展操作
	public function getPDOStatement(){
		return $this->sth;
	}

	// 检查PDO是否有PDO错误信息
	public function hasPDOError() {
		if ($this->dbh->errorCode() != '00000')
			return 1;
		else
			return 0;
	}

	// 获取PDO错误代码
	public function getErrCode() {
		return $this->dbh->errorCode();
	}

	//获取PDO错误信息
	public function getPDOError() {
		$error = $this->dbh->errorInfo();
		return $error[2];
	}


	//为字段名处理特殊字符，类工具
	public static function fmtName($str){
		$str=str_replace("`","``", $str);
		return $str;
	}


	/*	去除sql语句尾部的结束符

		结束符默认为分号，可通过$delim参数更换为其它字符
	 */
	public static function trimsql($sql, $delim=';'){
		return rtrim($sql, " \t\r\n\0\v".$delim);
	}

	/*	执行一句sql语句, 并设置$this->sth (PDOStatement) 对象, 为后续操作做准备

		当sql语句可带？参数或 :参数，
		参考 getRs()用法

		出错时返回false, 否则返回true
	*/
   public function query($sql, $params=null) {
		if(!is_array($params)) {
			$this->sth=$this->dbh->query($sql);
			if($this->sth===false)
				return false;
			return true;
		}
		//带？参数方式----
		$this->sth=$this->dbh->prepare($sql);
		if($this->sth===false) return false;

		return $this->sth->execute( $params ) ;
	}



	/*	查询数据,出错返回FALSE!

		参数：
			$sql: sql语句，可带？参数，或 :name 参数
			$params: 参数数组，当sql中包含参数时必须！
			$isrow: 是否只返回一行
	 */
	public function getRs($sql, $params=null, $isrow=false)	{
		//if(is_array($params)) { //带参数的sql
			//$this->sth=$this->dbh->prepare($sql); //获取PDOStatement 对象
			//if($this->sth===false)	return false;
			//$this->sth->execute( $params );		//执行查询语句
		//}else {	//无参数的sql
			//$this->sth = $this->dbh->query($sql);
		//}

		//if($this->hasPDOError())	return false;
		if($this->query($sql,$params)===false)
			return false;

		//self::setFetchMode($this->sth, $fetchboth);

		if($isrow) {
			$result = $this->sth->fetch();
			$this->sth->closeCursor();
		} else {
			$result = $this->sth->fetchAll();
		}
		if($this->hasPDOError())	return false;
		return $result;
	}

	/*	获取一行数据

		参数：类似 getRs()，无需isrow参数
	 */
	public function getRow($sql, $params=null) {
		return $this->getRs($sql,$params, 1);
	}

	//获取查询结果的行数
	// $sql中不需要用count！
	/*
		sql语句中带？参数时， $sql_or_arr参数必须为两元数组：
		$sql_or_arr[0]为sql语句，$sql_or_arr[1]为输入参数的数组；
	*/
	public function getRowCount($sql_or_arr,$debug=0) {
		if(is_array($sql_or_arr))
			$sql=$sql_or_arr[0];
		else
			$sql=$sql_or_arr;

		$sql= trim(trim($sql),';'); //去掉sql中收尾的";"
		$sql="select count(1) as count from ($sql) as X";
		if(is_array($sql_or_arr))
			$sql_or_arr[0]=$sql;
		else
			$sql_or_arr=$sql;

		if ($debug)
			echo "sql in getRowCount=$sql\n";

		$result=$this->getRow($sql_or_arr);
		if($this->hasPDOError())
			return false;

		return $result['count'];
	 }

	/*	根据关系数组，插入一条记录

		参数：
			$info：关联数组，每个key表示字段名，value表示字段值
			$table:表名

		出错返回false, 否则返回最后插入记录的ID（貌似只对整数类型ID有效）
	 */
	public  function createbyarr($info,$table,$debug=0){
		$keys=[];
		$values=[];
		//将字段名、
		foreach($info as $key=>$value){
			$keys[]='`'.self::fmtName($key).'`';
			$values[]=$this->quote($value);
		}

		$keys=join(',', $keys);
		$values=join(',', $values);
		$table=self::fmtName($table);
		$sql = "insert into `$table` ($keys) values($values)";
		$result= $this->dbh->exec($sql);
        if($debug && false === $result){
            global $logger;
            if ($logger){
                $logger->error("sql",[$sql]);
            }else{
                echo( "sql=$sql\n");
            }
        }
		if ($result===false)
			return false;
		return $this->dbh->lastInsertId();
	}


	/*	删除指定条件的记录

		条件不允许为空！
		条件中带参数时，需要传入$params参数
		出错返回false, 否则返回删除的记录数
	 */
	public  function del($table, $cond='', $params=null) {

		if ($cond=='')
			return false;

		$table=self::fmtName($table);
		$sql = "delete from `$table` where $cond";
		return $this->exec($sql, $params);
	}


	/*	用关系数组中的值，更新符合条件的记录

		参数类似createbyarr，$cond表示条件字串（前后不需要加空格）
		出错返回false, 否则返回更改的记录数
	 */
	public function updatebyarr($info,$table,$cond,$debug=0){
		$changes=[];
		foreach ($info as $key=>$value){
			$changes[]= "`". self::fmtName($key)."`=". $this->quote($value);
		}
		$changes=join(',', $changes);
		$table=self::fmtName($table);
		$sql = "update `$table` set $changes where $cond";

		$result= $this->dbh->exec($sql);
        if($debug && false === $result){
            global $logger;
            if ($logger){
                $logger->error("sql",[$sql]);
            }else{
                echo( "sql=$sql\n");
            }
        }
		return $result;
	}


	/*	获取数据记录数

		参数：
		$sql： "select ... from"开头的语句；可带参数（参考getRs()方法）
		$params: 配合$sql的参数数组（参考getRs()方法）

		返回：
		错误时返回false; 成功则返回行数；
	 */
	public function countsql($sql, $params=null) {


		//分析sql，提取 select ...from 之间以及之后的部分
		$s1=stripos($sql,'select');
		$s2=stripos($sql,'from');
		if($s1===false or $s2===false)
			return false;

		$fields=substr($sql, $s1+6, $s2-$s1-6); //select...from之间的部分
		$sqlAfterFrom=substr($sql, $s2+4);	// from 之后的部分

		//获取数据行数---
		$count_sql="select count(*) as count from ".$sqlAfterFrom;
		//echo "count_sql=$count_sql\n";
		$result=$this->getRow($count_sql,$params);
		if($result===false) return false;
		return $result['count'];
	}



	/*	获取指定页的数据记录数

		参数：
		$sql： "select ... from"开头的语句，尾部不可带limit子句，可带参数（参考getRs()方法）
		$pagesize: 每页行数，默认=10
		$page: 页码，从1开始，默认=1
		$params: 配合$sql的参数数组（参考getRs()方法）

		返回：
		错误时返回false; 成功则返回如下结构：
		[
			'count'=>99,	//总记录数
			'page'=>1,		//当前页码，从1开始
			'pagesize'=>20,	//每页行数
			'maxpage'=>5,	//最大页码/总页数
			'rows'=>[ ... ],		//各行数据记录的数组
		]


	 */
	public function fetch_page($sql, $page=1, $pagesize=10, $params=null) {


		//格式化page参数
		$page=intval($page);
		if($page<1)
			$page=1;

		//格式化 pagesize 参数
		$pagesize=intval($pagesize);
		if($pagesize<1)
			$pagesize=10;

		//获取数据行数
		$count=$this->countsql($sql,$params);
		if($count===false)
			return false;

		//获取数据---
		$query_sql=$this->trimsql($sql); //去除尾部分号
		$query_sql.= " limit ".($page-1)*$pagesize.",". $pagesize;
		$result=$this->getRs($query_sql,$params);
		if($result===false) return false;

		//返回数据
		return [
			'count'=>$count,
			'pagesize'=>$pagesize,
			'page'=>$page,
			'maxpage'=>ceil($count/$pagesize),
			'rows'=>$result,
		];

	}//function  fetch_page

    /*	获取指定页的数据记录数, 可修改查询记录数的sql！

		用法与fetch_page()类似，只多一个$countsql参数，需要提供一个查询记录总数的sql语句
	 */
    public function fetch_page2($sql, $countsql, $page=1, $pagesize=10, $params=null, $debug=null) {


        //格式化page参数
        $page=intval($page);
        if($page<1)
            $page=1;

        //格式化 pagesize 参数
        $pagesize=intval($pagesize);
        if($pagesize<1)
            $pagesize=10;

        if($debug) echo "--- in fetch_page2() ---\n";

        //获取数据行数
        if($debug) echo "countsql=$countsql\n";
        $res=$this->getRow($countsql,$params);
        if($res===false)
            return false;
        $count=$res['count'];

        //获取数据---
        $query_sql=$this->trimsql($sql); //去除尾部分号
        $query_sql.= " limit ".($page-1)*$pagesize.",". $pagesize;
        if($debug)
            echo "query_sql@fetch_page=$query_sql###";

        $result=$this->getRs($query_sql,$params);
        if($result===false) return false;

        //返回数据
        return [
            'count'=>$count,
            'pagesize'=>$pagesize,
            'page'=>$page,
            'maxpage'=>ceil($count/$pagesize),
            'rows'=>$result,
        ];

    }//function  fetch_page2

	//根据当前连接的数据库，转义一个参数值为合适的SQL语句中可用的字串
	public function quote($str){
		return $this->dbh->quote($str, PDO::PARAM_STR );
	}


	/*	执行一句sql语句

		当sql语句可带？参数或 :参数，
		参考 getRs()用法

		出错时返回false, 否则返回受影响数据行数
	*/
   public function exec($sql, $params=null) {
		if(!is_array($params)) {
			return $this->dbh->exec($sql);
		}
		//带？参数方式----
		$this->sth=$this->dbh->prepare($sql);
		if($this->sth===false) return false;

		if( $this->sth->execute( $params ) ) return $this->sth->rowCount();
		return false;
	}

	// $this->exec方法的别名
	public function execUpdate($sql, $params=null) {
		return $this->exec($sql,$params);
	}

	public function beginTransaction(){
	    $this->dbh->beginTransaction();
	}

	public function commit(){
	    $this->dbh->commit();
	}

	public function rollBack(){
	    $this->dbh->rollBack();
	}

} //class



